#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import locale
import os
import shlex
import stat
import subprocess
import time

import pexpect

from common.common import read_result_file, execute_cmd, execute_cmd_list, is_aix
from common.const import CMDResult, ExecuteResultEnum
from oracle import logger
from oracle.common.common import (get_dir_levels, get_database_node_list, get_last_log_scn, get_log_time,
                                  send_params_to_pexpect)
from oracle.common.constants import ScriptExitCode, OracleInstallType
from oracle.common.user_env_common import get_oracle_user_env_by_linux, build_user_env


def execute_linux_rman_cmd(pid, params, enc_type, sql_file, rst_file):
    """
    在windows中执行rman指令，输出结果文件，输出错误具体详情文件，输出错误码
    错误具体详情文件在stmp路径下，以result_errordetail_{pid}的形式保存
    :param pid: 请求id
    :param params: 包括"is_enc_bk"(int: 0, 1)，"instance name"(str)，
                            "db_user"(str)，"db_password"(str)和"rman_enc_section"五个字段的字典
    :param enc_type: enc_type
    :param sql_file: 执行命令语句的.sql文件路径
    :param rst_file: 输出的结果文件路径
    :return: 执行语句的错误码信息，0为成功（不输出错误具体详情文件）
    """
    # 解析params信息
    is_enc_bk = params.get("is_enc_bk", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")
    rman_enc_section = params.get("rman_enc_section", "")
    db_install_user = params.get("db_install_user", "")

    if is_enc_bk != 0 and enc_type == 1:
        origin_backup_sql = read_result_file(sql_file)
        rman_enc_section += origin_backup_sql
        write_file(sql_file, rman_enc_section)

    # 执行命令
    if db_password:
        db_password = f'"{db_password}"'
    os.environ["ORACLE_SID"] = instance_name
    oracle_user_env = get_oracle_user_env_by_linux(instance_name)
    if not db_user or not db_password:
        cmd = (f"su - {db_install_user} -c '{oracle_user_env}"
               f" rman target / cmdfile {sql_file} log {rst_file}'")
        logger.info(f"rman cmd id {cmd}")
    else:
        cmd = (f"su - {db_install_user} -c '{oracle_user_env}"
               f" rman target {db_user}/ cmdfile {sql_file} log {rst_file}'")
        logger.info(f"rman cmd db_install_user {db_install_user}, db_user: {db_user}, "
                    f"sql_file: {sql_file}, oracle_user_env:{oracle_user_env}")
    return_code, result = execute_linux_cmd(pid, cmd, db_password)
    if not return_code:
        # 输出错误码以及错误详情文件
        logger.error(f"Execute oracle rman cmd failed! pid = {pid}")
        return ScriptExitCode.ERROR_ORACLE_EXERMAN_FAILED
    return ScriptExitCode.SUCCESS


def execute_linux_cmd(pid, cmd, input_cmd: str):
    """
    在linux中执行cmd并获取其打印信息
    :param pid: 请求id
    :param cmd: 将要执行的cmd命令
    :param input_cmd: 将要与命令行进行交互的输入，为空则无交互命令；当要进行多次交互时，以\n进行分割
    :return: 返回执行命令行是否成功，以及其打印的信息
    """
    result = ""
    encoding = locale.getdefaultlocale()[1]
    cmd_list = shlex.split(cmd)
    try:
        process = subprocess.Popen(cmd_list, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except FileNotFoundError:
        logger.error("Execute command failed! Check if the command is correct or not! Pid = %s", pid)
        return False, result
    if input_cmd:
        process.stdin.write(input_cmd.encode())
    output, error = process.communicate(timeout=None)
    if error:
        logger.error("Execute linux command failed! pid = %s; error info: %s", pid, error.decode(encoding=encoding))
        return False, result
    result = output.decode(encoding=encoding)
    logger.info("Execute linux command succeed! pid = %s", pid)
    return True, result


def get_linux_oracle_version(oracle_install_user):
    oracle_user_env = build_user_env(user_name=oracle_install_user)
    sqlplus_cmd = f"{oracle_user_env}echo exit | sqlplus /nolog"
    output = subprocess.check_output(["su", "-", f"{oracle_install_user}", "-c", sqlplus_cmd])
    ora_version = ""
    for line in output.splitlines():
        line = line.decode("utf-8")
        if "SQL*Plus" in line:
            ora_version = line.split(" ")[2]
            break

    ora_pre_version = ora_version[0:2]
    ora_pre_version_extend = ora_version[0:4].replace('.', '')

    logger.info(f"Get oracle version({ora_version}), preVersion({ora_pre_version}), "
                f"preVersionExtend({ora_pre_version_extend}) success.")
    return [ora_version, ora_pre_version, ora_pre_version_extend]


def get_min_of_last_log_scn_by_thread(params, log_is_backed_up, job_id):
    pid = params.get("pid")
    instance_name = params.get("instance_name")
    install_user = params.get("db_install_user")
    logger.info(
        f"Get min of last log scn by thread, pid:{pid}, instance name:{instance_name}, install_user:{install_user}.")
    # RAC场景下有多个thread，此函数获取每个thread日志scn中最小的那个scn
    node_list = get_database_node_list(params, job_id)
    if not node_list:
        logger.error(f"Get node list failed, pid: {pid}")
        raise Exception("Get node list failed")

    # oracle 19c的scn最大值是2^48，此处取一个比它大的值
    scn_min = "300000000000000"
    last_log_time = ""
    min_scn_log_info = []
    for node in node_list:
        last_log_scn = get_last_log_scn(params, node, log_is_backed_up, job_id)
        if not last_log_scn:
            logger.warn(f"get last log scn empty")
            continue
        logger.info(f"Last log scn is {last_log_scn}, scn_min:{scn_min}, pid:{pid}.")
        if int(last_log_scn) < int(scn_min):
            scn_min = last_log_scn
            last_log_time = get_log_time(params, node, log_is_backed_up, scn_min)
    if int(scn_min) < 300000000000000:
        min_scn_log_info.append(scn_min)
        min_scn_log_info.append(last_log_time)
    logger.info(f"Get min of last log scn by thread success, pid:{pid}, min_scn_log_info:{min_scn_log_info}")
    return min_scn_log_info


def add_unix_timestamp(date):
    time_array = time.strptime(date, "%Y-%m-%d_%H:%M:%S")
    timestamp = time.mktime(time_array)

    return int(timestamp)


def write_file(file_path, content):
    flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(file_path, flags, modes), 'w') as out_file:
        out_file.write(content)


def check_oracle_install_type():
    db_is_cluster = OracleInstallType.DEFAULT
    _, sys_name, _ = execute_cmd("uname -s")
    oracle_loc = "/etc/oracle/ocr.loc"
    if sys_name == "HP-UX" or sys_name == "SunOS":
        oracle_loc = "/var/opt/oracle/ocr.loc"
    if not os.path.exists(oracle_loc):
        logger.warning(f"Can not find file:{oracle_loc} set default type single 0")
        return db_is_cluster
    for line in open(oracle_loc):
        if not line.startswith("local_only"):
            continue
        cluster_flag = line.split("=")[1].strip().upper()
        if cluster_flag == "FALSE":
            db_is_cluster = OracleInstallType.RAC
        elif cluster_flag == "TRUE":
            db_is_cluster = OracleInstallType.RESTART_SINGLE_PLUS_ASM
        else:
            logger.warning(f"Unknown oracle type: ${cluster_flag} set default type single 0 ")
    logger.info(f"Check oracle type [DBISCLUSTER={db_is_cluster}] success.")
    return db_is_cluster


def get_asm_instance():
    _, sys_name, _ = execute_cmd("uname -s")
    if sys_name == "SunOS":
        awk = "nawk"
    else:
        awk = "awk"
    cmd = ["ps -ef", "grep asm_...._+", "grep -v grep", awk + " -F_ '{print $NF}'", "uniq"]
    ret_code, asm_instance_name, ret_err = execute_cmd_list(cmd)
    logger.info(f"cmd:{cmd},ret_code:{ret_code},asm_instance_name:{asm_instance_name},ret_err:{ret_err}")
    return asm_instance_name.strip()


def execute_linux_sqlplus_cmd(pid, params, sql_file):
    """
    linux环境,sqlplus执行sql文件
    :param pid: 请求id
    :param params: 包括"instance name"(str)，"db_user"(str)，"db_password"(str)，"db_install_user"(str)
    :param sql_file: 执行命令语句的.sql文件路径
    :return: 执行语句的错误码信息，0为成功（不输出错误具体详情文件）
    """
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")
    db_install_user = params.get("db_install_user", "")

    if db_password:
        db_password = f'"{db_password}"'
    os.environ["ORACLE_SID"] = instance_name
    oracle_user_env = get_oracle_user_env_by_linux(instance_name)
    if not db_user or not db_password:
        cmd = (
            f"su - {db_install_user} -c '{oracle_user_env}"
            f"sqlplus / as sysdba @{sql_file}'")
    else:
        cmd = (
            f"su - {db_install_user} -c '{oracle_user_env}"
            f"sqlplus {db_user}/ as sysdba @{sql_file}'")
    logger.info(f"oracle sql cmd, db_install_user {db_install_user}, db_user: {db_user}, "
                f"sql_file: {sql_file}, oracle_user_env:{oracle_user_env}")
    return_code, result = execute_linux_cmd(pid, cmd, db_password)
    if not return_code:
        logger.error(f"Execute oracle sql cmd failed! cmd = {cmd}, result is {result}")
        return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
    return ScriptExitCode.SUCCESS


def check_oracle_install_type_linux():
    db_is_cluster = OracleInstallType.DEFAULT
    _, sys_name, _ = execute_cmd("uname -s")
    oracle_loc = "/etc/oracle/ocr.loc"
    if sys_name == "HP-UX" or sys_name == "SunOS":
        oracle_loc = "/var/opt/oracle/ocr.loc"
    if not os.path.exists(oracle_loc):
        logger.warning(f"Can not find file:{oracle_loc} set default type single 0")
        return db_is_cluster
    for line in open(oracle_loc):
        if not line.startswith("local_only"):
            continue
        cluster_flag = line.split("=")[1].strip().upper()
        if cluster_flag == "FALSE":
            db_is_cluster = OracleInstallType.RAC
        elif cluster_flag == "TRUE":
            db_is_cluster = OracleInstallType.RESTART_SINGLE_PLUS_ASM
        else:
            logger.warning(f"Unknown oracle type: ${cluster_flag} set default type single 0 ")
    logger.info(f"Check oracle type [DBISCLUSTER={db_is_cluster}] success.")
    return db_is_cluster


def check_sqlplus_status_linux(db_install_user):
    oracle_user_env = get_oracle_user_env_by_linux()
    cmd_list = f"su - {db_install_user} -c '{oracle_user_env}which sqlplus'"
    logger.info(f"check_sqlplus_status {db_install_user}, oracle_user_env:{oracle_user_env}")
    return_code, std_out, std_err = execute_cmd(cmd_list)
    if return_code != CMDResult.SUCCESS.value:
        logger.error(f"check_sqlplus_status failed! std_out {std_out}, std_err is {std_err}")
        return False
    if 'no sqlplus in' in std_out:
        logger.error(f"check_sqlplus_status failed! std_out {std_out}, std_err is {std_err}")
        return False
    return True


def check_rman_status_linux(db_install_user):
    oracle_user_env = get_oracle_user_env_by_linux()
    cmd_list = f"su - {db_install_user} -c '{oracle_user_env}which rman'"
    logger.info(f"check_rman_status {db_install_user}, oracle_user_env:{oracle_user_env}")
    return_code, std_out, std_err = execute_cmd(cmd_list)
    if return_code != CMDResult.SUCCESS.value:
        logger.error(f"check_rman_status failed! std_out {std_out}, std_err is {std_err}")
        return False
    if 'no rman in' in std_out:
        logger.error(f"check_sqlplus_status failed! std_out {std_out}, std_err is {std_err}")
        return False
    return True


def get_asm_install_user(param_asm_install_user):
    try:
        from os import stat as os_stat
        from pwd import getpwuid
        if param_asm_install_user:
            return param_asm_install_user
        cmd_list = [f"cat /etc/oracle/olr.loc", f"grep 'crs_home'"]
        return_code, std_out, err_str = execute_cmd_list(cmd_list)
        logger.info(f"return_code={return_code},std_out={std_out},err_str={err_str}")
        if int(return_code) != ExecuteResultEnum.SUCCESS.value:
            return "grid"
        crs_home_path = std_out.split('=')[1].strip()
        return getpwuid(os_stat(f"{crs_home_path}/bin/oracle").st_uid).pw_name
    except Exception as err_str:
        logger.warn(f"get_asm_install_user error,{err_str}")
        return "grid"


def check_asm_path(asm_path, grid_user, grid_env):
    dg_name_child = os.path.basename(asm_path) + '/'
    cmd = f"su - {grid_user} -c '{grid_env} asmcmd ls {os.path.dirname(asm_path)}'"
    logger.info(f"check linux disk group exits, cmd {cmd}")
    return_code, std_out, std_err = execute_cmd(cmd)
    if not std_out:
        logger.info(f"Check linux dist group not exist, std_out={std_out}, std_err={std_err}.")
        return False
    logger.info(f"std_out={std_out}")
    folders = std_out.split("\n")
    folders = [folder for folder in folders if folder != '']
    folders = [folder.upper() for folder in folders]
    logger.info(f"folders={folders},dg_name_child:{dg_name_child}")
    if dg_name_child in folders:
        return True
    if dg_name_child.upper() in folders:
        return True
    return False


def create_asm_dir(asm_path, grid_user, grid_env):
    parts = get_dir_levels(asm_path)
    path_str = parts[0]
    for part in parts[1:]:
        path_str = os.path.join(path_str, part)
        if check_asm_path(path_str, grid_user, grid_env):
            continue
        cmd = f"su - {grid_user} -c '{grid_env} asmcmd mkdir {path_str}'"
        return_code, std_out, std_err = execute_cmd(cmd)
        logger.info(f"ret_code:{return_code},std_out:{std_out},std_err:{std_err},cmd:{cmd}")
        if int(return_code) != 0:
            logger.error(f"create_asm_path {path_str} error,std_out:{std_out},std_err:{std_err}")
            return False
    return True


def execute_script(exec_script_cmd, params, timeout=None):
    if is_aix():
        cmd_list = [
            f"echo '{json.dumps(params)}'",
            exec_script_cmd
        ]
        return_code, std_out, std_err = execute_cmd_list(cmd_list, timeout)
        logger.debug(f"return_code:{return_code},std_out:{std_out},std_err:{std_err}")
        return int(return_code)
    else:
        child = pexpect.spawn(exec_script_cmd, encoding='utf-8', timeout=timeout)
        send_params_to_pexpect(params, child)
        index = child.expect([pexpect.TIMEOUT, pexpect.EOF])
        if index == 0:
            child.close()
            logger.error(f"execute {exec_script_cmd} timeout ,child.exitstatus={child.exitstatus}.")
            return child.exitstatus
        child.close()
        return child.exitstatus
